<?php
/** Abstract class for array types
*   The ArrayType does not support any properties other than its values,
*       so accessing elements using Array or Object notation is equivalent
*
*/
abstract class Yz_Array_Abstract
    extends Yz_Type
    implements Iterator,ArrayAccess,Countable,Yz_Array_Interface
{

    /**
     *  list of messages for the type checker
     *  @var array
     */
    protected static $expects = array(
        'test'                => 'array or Traversable',
        'testNotEmpty'        => 'non-empty array or Traversable',
    );

    /**
     *  the real values
     *   @var array value
     */
    protected $value = array();

    /******************************
    ***     Public methods      ***
    *******************************/

    /**
     *   @param mixed value
     */
    public function __construct( $value=null )
    {
        if ( null !== $value ) {
            $this->value = static::test( $value );
        }
    } // __construct


    /** a chainable constructor
     *  @param array-type value
     *  @return chainable
     */
    public static function fromArray($array)
    {
        $res = new static;
        $res->value = static::test( $array );
        return $res;
    }



    /** toString()
    *
    *   @return string
    */
    public function __toString()
    {
        $cl = $this->className();
        $count = $this->count();
        return "Object({$cl}[{$count}])";
    } // toString


    /** valueOf():
    *   return the value as array if possible
    *   Supports PHP native arrays, stdclass, ArrayObject, Traversable, and of course Yz_Array_Interface
    *
    *   @param mixed $value
    *   @return null|array value
    */
    public static function valueOf( $value )
    {
        switch ( true ) {
            case is_array($value):
                $val = $value;
                break;
            case $value instanceof Yz_Class:
                $val = $value->toArray();
                break;
            case $value instanceof stdclass:
                // convert to an array
                $val = get_object_vars($value);
                break;

            case $value instanceof ArrayObject:
                $val = $value->getArrayCopy();
                break;
            case $value instanceof Traversable:
                $val = iterator_to_array($value, true);
                break;

            default:
                $val = null;
        }
        return $val;
    } // valueOf


    /**
     *  test if a value is a non-empty array-type
     *  @return array
     */
    public static function testNotEmpty( $value, $context=null )
    {
        return static::doTest( $value, $context, 'testNotEmpty', function( $val ) {
            return count($val) > 0;
        } );
    }

    /**
     *  return the native PHP-array contained in this.value
     *      (thus, the same as getValue(), but with a more comprehensible name)
     *  A method of Yz_Array_Interface
     *  @return array
     */
    public function toArray()
    {
        return $this->value;
    } // toArray


    /** test whether a value is the same object as this
    *
    * @param mixed value
    * @return bool true if value is identical to $this
    */
    public function equals( $value )
    {
        return ( $value === $this );
    } // equals


    /** isEmpty()
    *   @return bool
    */
    public function isEmpty()
    {
        return 0 === count( $this->value );
    } // isEmpty


    /**
    *  Countable method
    *   @return int
    */
    public function count()
    {
        return count( $this->value );
    }


    /**
    *  Alias for count()
    *   @return int
    */
    public function length()
    {
        return $this->count();
    }


    /**
    *  Iterator methods
    */
    public function rewind()
    {
        reset($this->value);
        return $this;
    }

    public function current()
    {
        return current($this->value);
    }

    public function key()
    {
        return key($this->value);
    }

    public function next()
    {
        next($this->value);
        return $this;
    }

    public function valid()
    {
        return null !== key($this->value) ;
    }


    /***************************
     ***** other methods *******
     ***************************/

    /**
    *   Apply a callback to the internal value of a Yz_Type instance
    *
    *   Example using PHP53 lambda function feature:
    *       <code>
    *       $x = new Yz_String;
    *       $x->apply( function($value) { return strtolower($value); } );
    *       </code>
    *   @param callback: <type> callback(<type>)
    *   @return this
    *   @throws InvalidArgument if $callback is not a valid callback
    *   @throws InvalidArgument if callback returns an invalid value
    *   @throws Exception callback may throw smth but not recommended
    */
    public function map( $callback )
    {
        if( ! is_callable( $callback ) ) {
            throw new Yz_Exception_InvalidArgument();
        }
        $map = new $this;
        $mapvalue = array();
        if ( $this->value ) {

            // can't use array_map because it doesn't preserve keys
            foreach( $this->value as $index => $value ) {
                $mapvalue[$index] = call_user_func( $callback, $value );
            }
        }
        $map->value = $mapvalue;
        return $map;
    } // map


    /**
    *   Apply callback to all elements of the array:
    *       the old values will be replaced by those returned by the callback
    *   @param callback $callback: mixed callback( index, value )
    *   @return chainable
    *   @throw Yz_Exception_InvalidArgument if callback is not callable
    */
    public function apply( $callback )
    {
        debug();

        if ( ! is_callable( $callback ) ) {
            throw new Yz_Exception_InvalidArgument();
        }
        if ( $this->value ) {

            // can't use array_map because it doesn't preserve keys
            foreach( $this->value as $index => &$value ) {
                $value = call_user_func( $callback, $index, $value );
            }
        }
        return $this;
    } // apply


    /** filter values using a callback
    *       Remove all elements which fail the callback (callback returns false)
    *   @param callback $callback: bool callback( value ), or null for the default callback
    *       Default callback will remove any elements with a value of null,false,'' (empty string)
    *   @return chainable
    *   @throw Yz_Exception_InvalidArgument if callback is not callable
    */
    public function filter( $callback=null )
    {
        debug();
        if (null === $callback) {
            $callback = array($this, 'filterCallback');
        } else if( ! is_callable( $callback ) ) {
            throw new Yz_Exception_InvalidArgument();
        }
        if ( $this->value ) {
            $this->value = array_filter($this->value, $callback);
        }
        return $this;
    } // filter


    /** join the values with a string
     *
     *  @param string $join_str, default is comma
     *  @return string
    *   @throw Yz_Exception_InvalidArgument if str is not a scalar
    *   @todo what about non-string values?
     */
    public function join( $str="," )
    {
        debug();
        $str = Yz_String::test( $str );
        return join( $str, $this->value );
    } // join

    /** get an array containing some of the elements of the original array
     *
     *  @param int $from
     *  @param int|null $count
     *  @return self/Yz_Array_Abstract
     *  @todo check param types
     */
    public function slice( $from, $count=null )
    {
        debug();

        return $this->_slice($from, $count, false);
    } // slice


    /** get an array containing all of the elements of the input arrays
     *
     *  @param Yz_Array_Interface
     *  @return self/Yz_Array_Abstract
     *  @todo check param types
     */
    public function union( $array )
    {
        debug();

        $array = $this::test($array);

        $this->value = $this->value + $array;
        return $this;
    } // union


    /** get an array containing all of the elements of the input arrays
     *
     *  @param Yz_Array_Interface $input
     *  @param bool(false) $overwrite whether to preserve old values (false) or replace them with the new ones (true)
     *  @return self/Yz_Array_Abstract
     *  @todo check param types
     */
    public function unionRecursive( $input, $overwrite=false )
    {
        debug();

        // ensure that the input is a native PHP array
        $input = $this::valueOf($input);
        if (null === $input) {
            return $this;
        }

        $union = array();

        // first, iterate over the original keys/values
        // trying to union the values under existing keys
        // and then (possibly) removing these keys from input
        // actually, it doesn't matter for anything but speed
        foreach ($this->value as $index => $orig) {
            // by default, set out-value to original
            // it will be kept if either input or output is not array-type
            $out = $orig;
            if (array_key_exists($index, $input)) {
                if ($overwrite) {
                    $out = $input[$index];
                }
                $in = $this::valueOf($input[$index]);

                if (null !== $in) {
                    if ($orig instanceof Yz_Array_Interface) {
                        $out = $orig->unionRecursive($in, $overwrite);
                    } else if (is_array($orig)) {
                        // for native-array original value, we have to do unionRecursive
                        // using an instance of Yz_Hash
                        $out = Yz_Hash::fromArray($orig)
                            ->unionRecursive($in, $overwrite)
                            ->toArray();
                    }
                }
            }
            $union[$index] = $out;
        }

        // now that there are only new keys in input, we can safely append them
        // (even if not removed, the old keys are ignored)
        return new $this($union + $input);
    } // unionRecursive


    /**********************************
     ********   Protected methods *****
     *********************************/


    /**
     *  the real implementation of slice(), with choice whether to preserve keys
     *  @param int $from
     *  @param int|null $count
     *  @param bool $keys
     *  @return self/Yz_Array_Abstract
     *  @todo check param types
     */
    protected function _slice( $from, $count, $keys )
    {
        return new $this( array_slice($this->value, $from, $count, $keys) );
    }

    public function filterCallback($value)
    {
        return ! (null === $value || false === $value || "" === $value);
    }

    /** test if the index exists in Array
     *  @param string index (after adjustIndex)
     *  @return bool
     */
    protected function indexExists( $index )
    {
        return array_key_exists( $index, $this->value );
    }

    /** the default getter is called after index verification
     *  @param string index
     *  @return mixed|null
     */
    protected function defaultGetter( $index )
    {
        if ($this->indexExists( $index )) {
            return $this->value[$index];
        }
        return null;
    }

    /** the default setter is called after index verification
     *  @param string index
     *  @return chainable
     */
    protected function defaultSetter( $index, $value )
    {
        $this->value[$index] = $value;
        return $this;
    }

    /** the default unsetter is called after index verification
     *  @param string index
     *  @return chainable
     */
    protected function defaultUnsetter( $index )
    {
        unset($this->value[$index]);
        return $this;
    }

} // Yz_Array_Abstract
